/**********************************************************************

  Audacity: A Digital Audio Editor

  BufferedStreamReader.h

  Dmitry Vedenko

**********************************************************************/

#pragma once

#include <vector>
#include <cstddef>
#include <cstdint>
#include <type_traits>

/*!
 * \brief A facade-like class, that implements buffered reading from the underlying data stream.
 *
 * BufferedStreamReader provides optimal read performance for the built-in types of up to 8 bytes long.
 */
class UTILITY_API BufferedStreamReader /* not final */
{
public:
    static constexpr size_t RequiredAlignment = 8;

    explicit BufferedStreamReader(size_t bufferSize = 4096);

    //! Read up to maxBytes into the buffer. Returns the number of bytes read.
    size_t Read(void* buffer, size_t maxBytes);

    //! Read a single value of ValueType, where sizeof(ValueType) <= 8 and value is aligned to the size boundary
    template<typename ValueType>
    std::enable_if_t<sizeof(ValueType) <= RequiredAlignment, bool>
    ReadValue(ValueType& value) /* ReadData may throw */
    {
        constexpr size_t valueSize = sizeof(ValueType);
        const size_t availableBytes = mCurrentBytes - mCurrentIndex;

        // In case of underflow - just fall back to general Read routine
        if (availableBytes < valueSize) {
            return valueSize == Read(&value, valueSize);
        }

        // Special case for one bytes reads
        if constexpr (valueSize == 1) {
            value = mBufferStart[mCurrentIndex];
        } else {
            value = UncheckedRead<ValueType>();
        }

        mCurrentIndex += valueSize;

        return true;
    }

    //! Returns true if there is no more data available
    bool Eof() const;

    //! Read a single byte from the stream. Return -1 on failure.
    int GetC();

protected:
    //! Should return true, if underlying stream has more data
    virtual bool HasMoreData() const = 0;
    //! Read up to maxBytes into the buffer. Should return the number of bytes read.
    virtual size_t ReadData(void* buffer, size_t maxBytes) = 0;

private:
    //! Reads the data from the stream if there is not enough data buffered
    bool HandleUnderflow();

    /*! Reads a value of type T from the stream.
     * @pre mCurrentIndex + sizeof(T) <= mCurrentBytes
     */
    template<typename T>
    T UncheckedRead() noexcept
    {
        T result;

        // We know, that mBufferStart is always at least RequiredAlignment aligned
        // Only mCurrentIndex alignment matters here

        if ((mCurrentIndex % sizeof(T)) == 0) {
            // If the result is aligned - just read it from the memory
            const void* ptr = mBufferStart + mCurrentIndex;
            result = *static_cast<const T*>(ptr);
        } else {
            // Fallback to std::copy otherwise
            const uint8_t* begin = mBufferStart + mCurrentIndex;
            const uint8_t* end = begin + sizeof(T);
            void* out = &result;

            std::copy(begin, end, static_cast<uint8_t*>(out));
        }

        return result;
    }

    // Cache storage
    std::vector<uint8_t> mBufferData;

    // mBufferStart is aligned to RequiredAlignment bytes boundary.
    // Both values below are initialized in the constructor.
    uint8_t* mBufferStart;
    size_t mBufferSize;

    size_t mCurrentIndex { 0 };
    size_t mCurrentBytes { 0 };
};
